﻿using System;
using System.Text;
using System.Text.RegularExpressions;
using System.ComponentModel;
using Argos.Design;
using System.Drawing.Design;

namespace Argos.Scheduling
{
	/// <summary>
	/// Represents a time of day.
	/// </summary>
	[Serializable]
	[TypeConverter(typeof(TimeOfDayConverter))]
	public struct TimeOfDay : IComparable , IComparable<TimeOfDay>
	{

		#region #ctor: public TimeOfDay(int hour)

		/// <summary>
		/// Creates a new instance of the struct with the given hour component.
		/// </summary>
		/// <param name="hour">The hour component.</param>
		public TimeOfDay(int hour) : this(hour, 0, 0, 0, TimeKind.Local) { }

		#endregion

		#region #ctor: public TimeOfDay(int hour, int minute)

		/// <summary>
		/// Creates a new instance with the given hour and minute components.
		/// </summary>
		/// <param name="hour">The hour component.</param>
		/// <param name="minute">The minute component.</param>
		public TimeOfDay(int hour, int minute) : this(hour, minute, 0, 0, TimeKind.Local) { }

		#endregion

		#region #ctor: public TimeOfDay(int hour, int minute, int second)

		/// <summary>
		/// Creates a new instance with the given hour, minute and second components.
		/// </summary>
		/// <param name="hour">The hour component.</param>
		/// <param name="minute">The minute component.</param>
		/// <param name="second">The second component.</param>
		public TimeOfDay(int hour, int minute, int second) : this(hour, minute, second, 0, TimeKind.Local) { }

		#endregion

		#region #ctor: public TimeOfDay(int hour, int minute, int second, int millisecond)

		/// <summary>
		/// Creates a new instance with the given hour, minute, second and millisecond components.
		/// </summary>
		/// <param name="hour">The hour component.</param>
		/// <param name="minute">The minute component.</param>
		/// <param name="second">The second component.</param>
		/// <param name="millisecond">The millisecond component.</param>
		public TimeOfDay(int hour, int minute, int second, int millisecond) : this(hour, minute, second, millisecond, TimeKind.Local) { }

		#endregion

		#region #ctor: public TimeOfDay(int hour, int minute, int second, int millisecond, TimeKind kind)

		/// <summary>
		/// Creates a new instance with the given hour, minute, second, millisecond and kind components.
		/// </summary>
		/// <param name="hour">The hour component.</param>
		/// <param name="minute">The minute component.</param>
		/// <param name="second">The second component.</param>
		/// <param name="millisecond">The millisecond component.</param>
		/// <param name="kind">The kind of the time.</param>
		public TimeOfDay(int hour, int minute, int second, int millisecond, TimeKind kind)
		{
			_Hour = hour;
			_Minute = minute;
			_Second = second;
			_Millisecond = millisecond;
			_Kind = kind;
			_Ticks = 0;
		}

		#endregion

		#region #ctor: public TimeOfDay(DateTime dateTime)

		/// <summary>
		/// Creates a new instance of the struct from the given date and time.
		/// </summary>
		/// <param name="dateTime">The date and time to create the time struct from.</param>
		public TimeOfDay(DateTime dateTime)
		{
			_Hour = dateTime.Hour;
			_Minute = dateTime.Minute;
			_Second = dateTime.Second;
			_Millisecond = dateTime.Millisecond;
			_Kind = dateTime.Kind == DateTimeKind.Utc ? TimeKind.Utc : TimeKind.Local;
			_Ticks = dateTime.Ticks;
		}

		#endregion		



		#region public static bool operator >(TimeOfDay t1, TimeOfDay t2)

		/// <summary>
		/// Compares the two given instances and returns true if <paramref name="t1"/>
		/// is greater than <paramref name="t2"/>.
		/// </summary>
		public static bool operator >(TimeOfDay t1, TimeOfDay t2)
		{
			return string.Compare(t1.ToUtcTime().ToString(), t2.ToUtcTime().ToString()) > 0;
		}

		#endregion

		#region public static bool operator <(TimeOfDay t1, TimeOfDay t2)

		/// <summary>
		/// Compares the two given instances and returns true if <paramref name="t1"/>
		/// is less than <paramref name="t2"/>.
		/// </summary>
		public static bool operator <(TimeOfDay t1, TimeOfDay t2)
		{
			return string.Compare(t1.ToUtcTime().ToString(), t2.ToUtcTime().ToString()) < 0;
		}

		#endregion

		#region public static bool operator ==(TimeOfDay t1, TimeOfDay t2)

		/// <summary>
		/// Compares the two given instances and returns true if they are equal.
		/// </summary>
		public static bool operator ==(TimeOfDay t1, TimeOfDay t2)
		{
			return string.Equals(t1.ToString(), t2.ToString());
		}

		#endregion

		#region public static bool operator !=(TimeOfDay t1, TimeOfDay t2)

		/// <summary>
		/// Compares the two given instances and returns true if they are not equal.
		/// </summary>
		public static bool operator !=(TimeOfDay t1, TimeOfDay t2)
		{
			return !string.Equals(t1.ToString(), t2.ToString());
		}

		#endregion



		#region public static TimeOfDay Now { get; }

		/// <summary>
		/// Returns the current time of day.
		/// </summary>
		public static TimeOfDay Now
		{
			get { return new TimeOfDay(DateTime.Now); }
		}

		#endregion

		#region public static TimeOfDay UtcNow { get; }

		/// <summary>
		/// Returns the current time of day expresses in UTC (Coordinate Universal Time).
		/// </summary>
		public static TimeOfDay UtcNow
		{
			get { return new TimeOfDay(DateTime.UtcNow); }
		}

		#endregion

		#region public int Hour { get; set; }

		private int _Hour;

		/// <summary>
		/// Sets or returns the hour component of the time of day.
		/// </summary>
		public int Hour
		{
			get { return _Hour; }
			set
			{
				if (value < 0 || value > 23)
				{
					throw new ArgumentOutOfRangeException("Hour");
				}

				_Hour = value;
			}
		}

		#endregion

		#region public TimeKind Kind { get; set; }

		private TimeKind _Kind;

		/// <summary>
		/// Sets or returns the kind of time.
		/// </summary>
		public TimeKind Kind
		{
			get { return _Kind; }
			set { _Kind = value; }
		}

		#endregion

		#region public int Millisecond { get; set; }

		private int _Millisecond;

		/// <summary>
		/// Sets or returns the millisecond component of the time of day.
		/// </summary>
		public int Millisecond
		{
			get { return _Millisecond; }
			set
			{
				if (value < 0 || value > 999)
				{
					throw new ArgumentOutOfRangeException("Millisecond");
				}
				_Millisecond = value;
			}
		}

		#endregion

		#region public int Minute { get; set; }

		private int _Minute;

		/// <summary>
		/// Sets or returns the minute component of the time of day.
		/// </summary>
		public int Minute
		{
			get { return _Minute; }
			set
			{
				if (value < 0 || value > 59)
				{
					throw new ArgumentOutOfRangeException("Minute");
				}
				_Minute = value;
			}
		}

		#endregion

		#region public int Second { get; set; }

		private int _Second;

		/// <summary>
		/// Sets or returns the second component of the time of day.
		/// </summary>
		public int Second
		{
			get { return _Second; }
			set
			{
				if (value < 0 || value > 59)
				{
					throw new ArgumentOutOfRangeException("Second");
				}
				_Second = value;
			}
		}

		#endregion



		private long _Ticks;
		private long Ticks { get { return _Ticks; } set { _Ticks = value; } }



		#region public static TimeOfDay Parse(string s)

		/// <summary>
		/// Parses the given string to a <see cref="TimeOfDay"/> instance.
		/// </summary>
		/// <param name="s">The string to parse.</param>
		/// <returns>Returns the <see cref="TimeOfDay"/> that was parsed from the given string.</returns>
		/// <exception cref="FormatException">The exception that is thrown if the given string cannot be parsed into a <see cref="TimeOfDay"/> instance.</exception>
		/// <exception cref="ArgumentNullException">The exception that is thrown if <paramref name="s"/> is null.</exception>
		public static TimeOfDay Parse(string s)
		{
			if (null == s)
			{
				throw new ArgumentNullException("s");
			}

			string todPattern = @"(?i)^\d{1,2}:\d{2}(:\d{2}){0,1}(.\d{1,3}){0,1}Z{0,1}$";
			if (!Regex.IsMatch(s, todPattern))
			{
				throw new FormatException("The given string cannot be parsed into a TimeOfDay instance.");
			}

			TimeOfDay t = new TimeOfDay();
			if (s.EndsWith("z", StringComparison.OrdinalIgnoreCase))
			{
				t.Kind = TimeKind.Utc;
				s = s.Substring(0, s.Length - 1);
			}
			string[] arr = s.Split(':');

			// There must be at least the hours and minutes in the string
			// so we can assume that the arr array contains always at least
			// two elements.
			t.Hour = int.Parse(arr[0]);
			t.Minute = int.Parse(arr[1]);

			if (arr.Length > 2)
			{
				string[] arr2 = arr[2].Split('.');
				t.Second = int.Parse(arr2[0]);
				if (arr2.Length > 1)
				{
					t.Millisecond = int.Parse(arr2[1]);
				}
			}

			return t;
		}

		#endregion

		#region public static bool TryParse(string s, out TimeOfDay t)

		/// <summary>
		/// Tries parsing the given string into a <see cref="TimeOfDay"/> struct.
		/// </summary>
		/// <param name="s">The string to try to parse.</param>
		/// <param name="t">Holds the parsed result if parsing is successful.</param>
		/// <returns>
		/// Returns <c>true</c> if the parsing is successful.
		/// </returns>
		public static bool TryParse(string s, out TimeOfDay t)
		{
			t = new TimeOfDay();
			try
			{
				t = TimeOfDay.Parse(s);
				return true;
			}
			catch
			{
				return false;
			}
		}

		#endregion



		#region public TimeOfDay Add(TimeSpan value)

		/// <summary>
		/// Returns a new <see cref="TimeOfDay"/> struct where the given
		/// time span has been added to the time of the current struct.
		/// </summary>
		/// <param name="value">The time span to add to the current time of day.</param>
		public TimeOfDay Add(TimeSpan value)
		{
			DateTime dt1 = this.ToDateTime();
			DateTime dt2 = dt1.Add(value);
			TimeOfDay tod = new TimeOfDay(dt2);
			tod.Kind = this.Kind;
			return tod;
		}

		#endregion

		#region public int CompareTo(object obj)

		/// <summary>
		/// Compares the given object to the current time instance.
		/// </summary>
		/// <param name="obj">The object to compare to.</param>
		public int CompareTo(object obj)
		{
			if (!(obj is TimeOfDay))
			{
				return 1;
			}

			return this.CompareTo((TimeOfDay)obj);
		}

		#endregion
		
		#region public int CompareTo(TimeOfDay other)

		/// <summary>
		/// Compares the given <see cref="TimeOfDay"/> struct to the current.
		/// </summary>
		/// <param name="other">The struct to compare to the current.</param>
		public int CompareTo(TimeOfDay other)
		{
			return this.ToUtcTime().ToString().CompareTo(other.ToUtcTime().ToString());
		}

		#endregion
		
		#region public override bool Equals(object obj)

		/// <summary>
		/// Compares the given object to the current time of day.
		/// </summary>
		/// <param name="obj">The object to compare the current time of day to.</param>
		/// <returns>Returns true if the given object equals the current time of day.</returns>
		public override bool Equals(object obj)
		{
			if (!(obj is TimeOfDay))
			{
				return false;
			}

			return this == (TimeOfDay)obj;
		}

		#endregion

		#region public override int GetHashCode()

		/// <summary>
		/// Returns the hash code for the current instance.
		/// </summary>
		public override int GetHashCode()
		{
			return this.ToString().GetHashCode();
		}

		#endregion

		#region public DateTime ToDateTime()

		/// <summary>
		/// Returns the current date with the time component from the current time of day instance.
		/// </summary>
		public DateTime ToDateTime()
		{
			return this.ToDateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
		}

		#endregion

		#region public DateTime ToDateTime(int year, int month, int day)

		/// <summary>
		/// Returns a <see cref="DateTime"/> struct where the date is specified from the given
		/// values and the time component is defined by the current time of day instance.
		/// </summary>
		/// <param name="year">The year component.</param>
		/// <param name="month">The month component.</param>
		/// <param name="day">The day component.</param>
		public DateTime ToDateTime(int year, int month, int day)
		{
			DateTimeKind kind = this.Kind == TimeKind.Local ? DateTimeKind.Local : DateTimeKind.Utc;
			if (this.Ticks > 0)
			{
				return new DateTime(this.Ticks, kind);
			}
			return new DateTime(year, month, day, this.Hour, this.Minute, this.Second, this.Millisecond, kind);
		}

		#endregion

		#region public TimeOfDay ToLocalTime()

		/// <summary>
		/// Returns the local representation of the current time.
		/// </summary>
		public TimeOfDay ToLocalTime()
		{
			DateTime dt = this.ToDateTime();
			return new TimeOfDay(dt.ToLocalTime());
		}

		#endregion
		
		#region public override string ToString()

		/// <summary>
		/// Returns the string representation of the time of day.
		/// </summary>
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();

			sb.AppendFormat("{0:00}", this.Hour);
			sb.AppendFormat(":{0:00}", this.Minute);

			if (this.Second > 0 || this.Millisecond > 0)
			{
				sb.AppendFormat(":{0:00}", this.Second);
			}

			if (this.Millisecond > 0)
			{
				sb.AppendFormat(".{0:000}", this.Millisecond);
			}

			if (this.Kind == TimeKind.Utc)
			{
				sb.Append("Z");
			}

			return sb.ToString();
		}

		#endregion			

		#region public TimeOfDay ToUtcTime()

		/// <summary>
		/// Returns the current time expresses in UTC (Coordinate Universal Time).
		/// </summary>
		public TimeOfDay ToUtcTime()
		{
			DateTime dt = this.ToDateTime();
			return new TimeOfDay(dt.ToUniversalTime());
		}

		#endregion

	}

	/// <summary>
	/// Defines different kinds of time.
	/// </summary>
	public enum TimeKind
	{
		/// <summary>
		/// Local time.
		/// </summary>
		Local,

		/// <summary>
		/// UTC time.
		/// </summary>
		Utc
	}

}
